﻿using System.Collections.Generic;
using System.Windows.Controls;
using System.Linq;
using System.Windows;
using DeepEarth.Core;
using System;
using DeepEarth.Map.Core;
using System.Collections.Specialized;
namespace DeepEarth.Toolkit.Drawing
{
    public class ShapeEditor
    {
        private int minLocations;

        private bool FirstAndLastIdentical
        {
            get
            {
                if (Locations.Count <= 2)
                {
                    return false;
                }
                return Locations[0] == Locations[Locations.Count - 1];
            }
        }

        // this flag is set when a point is actually moved. 
        // Used to prevent unnecessary redraws
        bool locationsDirtyFlag = false;

        Location midpointLocation = null;

        public IList<Location> Locations { get; set; }

        Panel drawingSurface;

        Func<Location, Point> locationToViewportPoint;
        Func<Point, Location> viewportPointToLocation;

        UIElement interactionSurface;

        public event Action EditCompleted;

        public ShapeEditor(UIElement interactionSurface, Panel drawingSurface, Func<Location, Point> locationToViewportPoint, Func<Point, Location> viewportPointToLocation)
        {
            this.interactionSurface = interactionSurface;
            this.drawingSurface = drawingSurface;
            this.locationToViewportPoint = locationToViewportPoint;
            this.viewportPointToLocation = viewportPointToLocation;

            this.drawpoints = new List<DrawPoint>();
        }

        public void Edit(IList<Location> locations)
        {
            this.Locations = locations;

            this.minLocations = FirstAndLastIdentical ? 3 : 2;

            Redraw();

            SubscribeToLocationChanges();
        }

        private void SubscribeToLocationChanges()
        {
            if (Locations is INotifyCollectionChanged)
            {
                INotifyCollectionChanged observable = Locations as INotifyCollectionChanged;
                observable.CollectionChanged += Locations_CollectionChanged;
            }
        }

        private void UnSubscribeFromLocationChanges()
        {
            if (Locations is INotifyCollectionChanged)
            {
                INotifyCollectionChanged observable = Locations as INotifyCollectionChanged;
                observable.CollectionChanged -= Locations_CollectionChanged;
            }
        }

        public void End()
        {
            UnSubscribeFromLocationChanges();
            Clear();
        }

        void Locations_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            Redraw();
        }

        private void Clear()
        {
            foreach (DrawPoint p in drawpoints)
            {
                drawingSurface.Children.Remove(p);
            }

            drawpoints.Clear();
        }

        private void Add(DrawPoint p)
        {
            this.drawingSurface.Children.Add(p);
            this.drawpoints.Add(p);
        }

        public void Redraw()
        {
            this.locationsDirtyFlag = true;
            DrawPoints();
        }

        List<DrawPoint> drawpoints;

        // TODO: Revisit this method
        private void DrawPoints()
        {
            if (!locationsDirtyFlag || this.Locations == null || !this.Locations.Any())
                return;

            Clear();

            Location prevLocation = Locations[0];

            // The start point
            DrawPointStart sPoint = new DrawPointStart(interactionSurface, viewportPointToLocation)
            {
                Index = 0
            };

            sPoint.SetValue(Layer.PositionOriginProperty, new PositionOrigin { X = 0.5, Y = 0.5 });

            AttachHandlers(sPoint);
            Add(sPoint);
            sPoint.SetValue(Layer.PositionProperty, Locations[0]);

            for (int i = 1; i < Locations.Count; i++)
            {
                // Don't draw a point for the last location in a closed shape where we were provided
                // a list of locations where the first and last locations are the same
                if (!FirstAndLastIdentical || (FirstAndLastIdentical && i != Locations.Count - 1))
                {
                    DrawPoint point = new DrawPoint(interactionSurface, viewportPointToLocation)
                    {
                        Index = i
                    };

                    AttachHandlers(point);
                    point.SetValue(Layer.PositionOriginProperty, new PositionOrigin { X = 0.5, Y = 0.5 });
                    Add(point);
                    point.SetValue(Layer.PositionProperty, Locations[i]);
                }

                DrawPointMid mPoint = new DrawPointMid(interactionSurface, viewportPointToLocation)
                {
                    Index = i
                };

                AttachHandlers(mPoint);
                mPoint.SetValue(Layer.PositionOriginProperty, new PositionOrigin { X = 0.5, Y = 0.5 });
                Location midLocation = GetLinearMidpoint(prevLocation, Locations[i]);
                Add(mPoint);
                mPoint.SetValue(Layer.PositionProperty, midLocation);

                prevLocation = Locations[i];
            }

            locationsDirtyFlag = false;
        }

        private Location GetLinearMidpoint(Location loc1, Location loc2)
        {
            Point a = locationToViewportPoint(loc1);
            Point b = locationToViewportPoint(loc2);

            Point mid = new Point((a.X + b.X) / 2, (a.Y + b.Y) / 2);

            return viewportPointToLocation(mid);
        }

        private void AttachHandlers(DrawPoint point)
        {
            point.DrawPointMoveStarted += point_DrawPointMoveStarted;
            point.DrawPointMoved += point_DrawPointMoved;
            point.DrawPointMoveCompleted += point_DrawPointMoveCompleted;
            point.DrawPointClick += point_DrawPointClick;
        }

        // Clicking a drawpoint removes it
        void point_DrawPointClick(object sender, DrawPointMovedEventArgs args)
        {
            if (sender is DrawPoint && Locations.Count > 2)
            {
                int targetIndex = args.Index;

                if (args.Index == 0)
                {
                    // If the startpoint and endpoint are the same, remove the endpoint.
                    var superimposed = Locations.Where(l => l.Latitude == Locations[0].Latitude && l.Longitude == Locations[0].Longitude).ToList();

                    if (superimposed.Count > 1)
                    {
                        targetIndex = Locations.Count - 1;
                    }
                }

                Locations.RemoveAt(targetIndex);

                // if there are only three points, and the first and last are the same, we should have a line, not a flat triangle
                if (Locations.Count == 3)
                {
                    if (Locations[0] == Locations[2])
                    {
                        Locations.RemoveAt(2);
                    }
                }

                Redraw();
            }

        }

        // If the shape is closed, then if the start point is being moved, the last point must be synchronized
        private bool synchronizeEndPoint = false;

        private void point_DrawPointMoveStarted(object sender, DrawPointMovedEventArgs args)
        {
            UnSubscribeFromLocationChanges();

            synchronizeEndPoint = sender is DrawPointStart && FirstAndLastIdentical;

            // If we're moving a midpoint marker, then add a new location to the polyline
            DrawPointMid midPoint = sender as DrawPointMid;
            if (midPoint != null)
            {
                midpointLocation = new Location
                {
                    Longitude = args.Location.Longitude,
                    Latitude = args.Location.Latitude,
                };
                Locations.Insert(args.Index, midpointLocation);
            }

            // Hide all of the point markers except for the one that we're moving
            foreach (DrawPoint d in drawingSurface.Children.OfType<DrawPoint>())
            {
                if (d != sender)
                {
                    d.Visibility = Visibility.Collapsed;
                }
            }
        }

        private void point_DrawPointMoveCompleted(object sender, DrawPointMovedEventArgs args)
        {
            SubscribeToLocationChanges();

            // If the user has not moved the mouse, then the midpoint location is still in 
            // our Locations collection and needs to be removed as it is not needed.
            //
            // If the user moved the mouse then the midpoint Location instance that we added
            // in the start move handler has been replaced in the drawPointMoved handler.
            // Therefore the locationsDirtyFlag check is strictly not required.
            if (midpointLocation != null && locationsDirtyFlag)
            {
                Locations.Remove(midpointLocation);
            }

            // Nothing was moved so we won't be doing a Redraw. Show the drawPoints that
            // were hidden in the start move handler
            if (!locationsDirtyFlag)
            {
                foreach (DrawPoint d in drawingSurface.Children.OfType<DrawPoint>())
                {
                    d.Visibility = Visibility.Visible;
                }
            }
            else
            {
                DrawPoints();
            }
        }

        private void point_DrawPointMoved(object sender, DrawPointMovedEventArgs args)
        {
            if (Locations.Count > args.Index)
            {
                // Indicate that something was moved, so when the user stops interaction
                // we should redraw all of the points
                locationsDirtyFlag = true;

                // trigger location change
                Locations[args.Index] = args.Location;

                // In a closed shape, the last point will not have a drawpoint marker.
                // If we were provided a list of locations where the first and last locations were identical,
                // synchronize the last with the first, which was just moved. (the last has no marker so cannot be moved though the UI)
                if ( synchronizeEndPoint )
                {
                    Location endpoint = Locations[Locations.Count - 1];
                    endpoint.Altitude = args.Location.Altitude;
                    endpoint.Longitude = args.Location.Longitude;
                    endpoint.Latitude = args.Location.Latitude;
                }
            }

            //TODO: Implement Autopanning
            //Utilities.AutoPan(args.Point, map);
        }
    }
}
